결과코드 체계 중앙화 설계 패턴
NOTE
시스템 전반에 흩어진 “결과코드→메시지” 변환 로직을 DB를 단일 소스로 삼아 중앙화하고, 컴파일 안전성(enum)과 운영 유연성(재배포 없는 메시지 수정)을 동시에 확보하는 설계 패턴.
1. 문제 — “코드는 던지는데 메시지를 채우는 중앙 지점이 없다”
여러 팀/시점에 걸쳐 성장한 시스템에서 흔히 발견되는 패턴:
방식 1: 코드 + 메시지를 매번 직접 하드코딩해서 던진다 → 메시지 오타·불일치가 흩어짐
방식 2: 코드만 던지고 메시지는 비워둔다 → 프론트가 fallback 문구("처리 실패")로 대체
방식 3: 성공 케이스만 공통 로직이 "0000/정상처리"를 자동 세팅한다코드-메시지 매핑 테이블이 DB에 이미 1,000건 이상 존재하는데도 백엔드 코드는 그 테이블을 한 번도 읽지 않는 경우가 흔하다 — 즉 데이터는 있는데 “그 데이터를 읽어서 응답에 채우는 공통 길목”이 없는 것이 진짜 문제다.
2. 핵심 설계 — DB 한 소스에서 두 갈래로 파생
CODE_TABLE(코드→메시지 매핑) ─┬─ (기동 시 런타임 로드) → Map<code, msg> ← 메시지 해석
└─ (빌드 시 codegen) → ResultCode enum ← 컴파일 안전계층 1 — 런타임 메시지 레지스트리 (DB 필수인 이유)
기동 시 SELECT code, message FROM CODE_TABLE WHERE ...로 메모리에 Map<String,String>을 적재하고, msgOf(code) 형태로 조회 메서드를 제공한다. 이 계층은 반드시 DB여야 한다 — 수천 건을 enum에 담을 수 없고, 운영팀이 메시지 오타를 고칠 때 재배포하면 안 되기 때문이다(운영팀이 DB만 고치면 재배포 없이 즉시 반영).
계층 2 — 코드 생성(codegen) enum (컴파일 안전)
코드 테이블 전체를 읽어 “코드 값(식별자)“만 담은 enum을 생성한다 — 메시지는 굽지 않는다.
CANCEL_DONE("2013"),
INSUFFICIENT_BALANCE("PA11"),
...메시지는 항상 계층 1의 런타임 Map에서 가져온다. 이렇게 하면 운영팀의 메시지 수정이 즉시 반영되면서도, 코드를 참조하는 쪽은 컴파일 시점에 오타를 잡을 수 있다.
동기화 부담이 없는 이유: 손으로 enum과 DB를 맞추는 게 아니라 한 소스(DB)에서 생성하므로, DB에 코드 추가 → 제너레이터 재실행 → enum에 상수 등장, 이 흐름만 지키면 된다.
한계: 언어가 문자열 리터럴 직접 사용(
"2013")을 막아주지는 않는다. 컴파일 안전은 enum을 실제로 사용했을 때만 보장되므로, “코드는 반드시 enum으로만 참조”를 코드 리뷰 규칙으로 강제하는 보완책이 필요하다.
3. 메시지 자동 채움 위치 — 공통 응답 길목 하나
빈/서비스는 코드만 던지고, 메시지는 공통 응답 처리 지점(예: 전역 ResponseBodyAdvice, 또는 그 프레임워크의 응답 조립 공통 유틸)에서 채운다. REST가 아닌 별도 프로토콜(소켓 등)이 있다면 그 경로의 응답 조립 지점에도 동일 로직을 추가해야 커버리지가 완성된다.
이렇게 하면 코드 곳곳에 흩어진 “정적으로 메시지를 직접 쓰는” 코드를 걷어낼 수 있다(단, 동적으로 생성해야 하는 메시지는 예외로 남긴다 — 아래 4절).
4. 우선순위 사다리 — 언제 채우고 언제 실패로 정규화할지
공통 응답 길목에서 아래 순서로 판단한다.
① 코드가 등록돼 있고 + 빈이 이미 메시지를 명시했음 → 코드 + 그 메시지(직접 쓴 것 존중)
② 코드가 등록돼 있고 + 메시지가 비어있음 → 코드 + DB 메시지로 채움
③ 코드가 등록 안 돼 있고 + 메시지는 명시돼 있음 → 코드 + 그 메시지 유지 + WARN 로그(패스스루 허용)
④ 코드가 등록 안 돼 있고 + 메시지도 없음 → 공통 실패 코드/메시지로 대체 + WARN
⑤ 코드 자체가 실패를 나타내지 않는 이상 상태 → 공통 실패 코드/메시지 + WARN판단 논리: 메시지까지 명시적으로 준 경우는 의도된 것(외부 시스템 패스스루 포함)으로 존중한다(③). 코드만 던졌는데 등록이 안 돼 있으면 누락/버그로 보고 공통 실패로 정규화한다(④). ③④⑤은 전부 WARN을 무조건 남겨 운영팀이 코드 테이블에 등록하도록 유도한다.
③이 필요한 이유(컴파일 안전이 닿지 않는 경계): 외부 시스템(PG/게이트웨이 등)의 응답 코드를 그대로 통과시키는 지점, UI에서 받은 값을 그대로 되돌리는 지점, 이관 과도기의 잔존 리터럴 등 — 이런 곳은 내부 코드 테이블에 없어도 정상 케이스다.
5. 코드값 분류 — “숫자 대역”이 아니라 “메타데이터 컬럼”으로 분류한다
레거시 코드 체계는 보통 숫자 범위로 도메인을 구분해왔다(예: 10xx=시스템, 20xx=취소 등). 이 레거시 범위를 갑자기 바꾸면(리넘버링) 외부 연동·UI가 그 값을 참조하고 있을 위험이 크다.
원칙: 기존 코드 값은 절대 리넘버링하지 않는다. 대신
- 기존 숫자 범위를 역설계해 “도메인별 예약 범위표”로 문서화한다.
- 분류(공통/도메인 구분)는 코드값 자체가 아니라 별도 분류 메타데이터 컬럼(있다면 그 컬럼)으로 표현한다.
- 새로 생기는 도메인은 비어있는 숫자 대역을 새로 배정한다.
- 알파벳/플레이스홀더 코드(개발자가 빈 숫자 대역을 못 찾아 즉흥적으로 만든 것들)는 외부 의존성이 확인된 것만 골라 나중에 숫자 대역으로 이관한다 — 확인 안 된 것은 값 고정 + 분류만 정비하고 그대로 둔다.
6. 적용 순서(일반화)
1. 런타임 레지스트리(DB 로드 Map) + 공통 응답 길목 자동 채움 ← 효과 최대, 위험 최소, 가장 먼저
2. codegen enum 도입 + 기동 시 검증
3. 도메인 예약 범위표 확정 + 분류 메타데이터 정비
4. 누락 코드를 코드 테이블에 등록(2번 검증이 목록을 자동 산출)
5. 리터럴 → enum 치환(도메인별로, 외부/UI 연동 지점은 테스트하며 신중히)
6. 애매한 범용 실패코드(예: 9999류) 남용 케이스를 의미별 코드로 분리
7. (후순위·위험) 알파벳/플레이스홀더 코드의 숫자 대역 이관 — 외부 의존성 확인된 것만이 접근은 결과코드뿐 아니라, “여러 곳에 흩어진 매핑 테이블을 하나의 진실원천으로 모으고 컴파일 안전과 운영 유연성을 동시에 얻고 싶은” 모든 상황(에러코드, 상태코드, 권한 코드 등)에 일반화해서 적용할 수 있다.
관련 문서
- (아키텍처) 점진적 인증 전환과 게이트웨이 책임분리 패턴 — 외부 게이트웨이 연동 시 “라우팅과 결과코드 체계만 내부 표준에 맞춘다”는 원칙으로 이 결과코드 중앙화 설계를 실제 게이트웨이 클라이언트 설계에 적용하는 사례